﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using DiskInspector.Model.MetaDataAttributes;

namespace DiskInspector.Model
{
    public static class DataParser
    {
        public static T Parse<T>(byte[] data)
        {
            return Parse<T>(data, 0);
        }

        public static T Parse<T>(byte[] data, int offset)
        {
            return (T)Parse(typeof(T), data, offset);
        }

        public static object Parse(Type type, byte[] data, int offset)
        {
            var instance = Activator.CreateInstance(type);

            var properties = type.GetProperties().Where(p => p.CustomAttributes.Any()).OrderBy(p => p.MetadataToken);

            if (offset != 0)
                data = data.Skip(offset).ToArray();

            Debug.Assert(data.Length > 0, "Invalid data length.");

            var bitArray = new BitArray(data);
            var bitOffset = 0;

            foreach (var property in properties)
            {
                int length = 0;

                var fieldLength = property.GetCustomAttribute<FieldLengthAttribute>();
                if (fieldLength != null)
                {
                    if (property.PropertyType.IsArray)
                    {
                        if (bitOffset % 8 != 0)
                            throw new InvalidOperationException("Array is not byte-aligned.");

                        switch (property.PropertyType.GetElementType().Name)
                        {
                            case "Byte":
                                SetByteArrayField(fieldLength, data, bitOffset / 8, property, instance);
                                break;
                            case "Char":
                                SetCharArrayField(fieldLength, data, bitOffset / 8, property, instance);
                                break;
                        }
                    }
                    else if (property.PropertyType.IsPrimitive)
                    {
                        SetField(fieldLength, bitArray, bitOffset, property, instance);
                    }
                    else 
                        throw new InvalidOperationException("Unrecognized property type.");

                    length = fieldLength.BitLength;
                }
                else
                {
                    var blockLength = property.GetCustomAttribute<BlockLengthAttribute>();
                    if (blockLength != null)
                    {
                        if (bitOffset % 8 != 0)
                            throw new InvalidOperationException("Block is not byte-aligned.");

                        SetBlock(blockLength, data, bitOffset / 8, property, instance);
                        length = blockLength.BitLength;
                    }
                }

                bitOffset += length;
            }

            return instance;
        }

        private static void SetByteArrayField(FieldLengthAttribute fieldLengthAttribute, byte[] data, int offset, PropertyInfo property, object instance)
        {
            byte[] value = data.Skip(offset).Take(fieldLengthAttribute.ByteLength).ToArray();
            property.SetValue(instance, value);
        }

        private static void SetCharArrayField(FieldLengthAttribute fieldLengthAttribute, byte[] data, int offset, PropertyInfo property, object instance)
        {
            char[] value = data.Skip(offset).Take(fieldLengthAttribute.ByteLength).Select(Convert.ToChar).ToArray();
            property.SetValue(instance, value);
        }

        private static void SetField(FieldLengthAttribute fieldLengthAttribute, BitArray bitArray, int bitOffset, PropertyInfo property, object instance)
        {
            ulong value = 0;
            for (int i = 0; i < fieldLengthAttribute.BitLength; i++)
            {
                ulong bit = Convert.ToUInt64(bitArray[bitOffset + i]);
                value = value | (bit << i);
            }

            switch (property.PropertyType.Name)
            {
                case "Byte":
                    property.SetValue(instance, (byte)value);
                    break;
                case "SByte":
                    property.SetValue(instance, (sbyte)value);
                    break;
                case "Int16":
                    property.SetValue(instance, (short)value);
                    break;
                case "UInt16":
                    property.SetValue(instance, (ushort)value);
                    break;
                case "Int32":
                    property.SetValue(instance, (int)value);
                    break;
                case "UInt32":
                    property.SetValue(instance, (uint)value);
                    break;
                case "Int64":
                    property.SetValue(instance, (long)value);
                    break;
                case "UInt64":
                    property.SetValue(instance, value);
                    break;
            }
        }

        private static void SetBlock(BlockLengthAttribute blockLengthAttribute, byte[] data, int offset, PropertyInfo property, object instance)
        {
            if (!property.PropertyType.IsArray)
                throw new InvalidOperationException("Property is not an array.");

            var value = Array.CreateInstance(property.PropertyType.GetElementType(), blockLengthAttribute.Count);

            var elementLength = blockLengthAttribute.ByteLength / blockLengthAttribute.Count;

            for (int i = 0; i < blockLengthAttribute.Count; i++)
            {
                var element = Parse(property.PropertyType.GetElementType(), data, offset);
                value.SetValue(element, i);
                offset += elementLength;
            }

            property.SetValue(instance, value);
        }
    }
}
